﻿using System;
using System.Collections.Generic;
using System.IO;

namespace WubiTableTransformers
{
    public sealed class MicrosoftWubiTable
    {
        private const string CodeEmpty = "Code can not be empty.";
        private const string CodeTooLong = "Code length must be less than or equal to 4.";
        private const string CodeNotLetterString = "Code must be lower case letter string.";
        private const string CodeContainsLetterZ = "Code can not contain letter 'z'.";
        private const string ValueEmpty = "Value can not be empty.";
        private const string ValueContainsWS = "Value can not contain white space.";
        private const string PairTooHuge = "Pair is too huge to be encoded in 65536 bytes.";
        private const string IndexTooLarge = "Index is too huge to be encode in 'uint16'.";

        private readonly SortedDictionary<string, HashSet<string>> records;

        public MicrosoftWubiTable()
        {
            this.records = new SortedDictionary<string, HashSet<string>>();
        }

        public void AddRecord(string code, string value)
        {
            if (code == null)
                throw new ArgumentNullException("code");
            if (value == null)
                throw new ArgumentNullException("value");
            HashSet<string> values;
            if (!records.TryGetValue(code, out values))
            {
                values = new HashSet<string>();
                records.Add(code, values);
            }
            values.Add(value);
        }

        public void Print(TextWriter writer)
        {
            if (writer == null)
                throw new ArgumentNullException("writer");
            foreach (var t in records)
            {
                if (t.Value.Count > 0)
                {
                    writer.Write(t.Key);
                    foreach (string value in t.Value)
                    {
                        writer.Write(' ');
                        writer.Write(value);
                    }
                    writer.WriteLine();
                }
            }
        }

        public void Print()
        {
            Print(Console.Out);
        }

        public void Load(TextReader reader, string fileName)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");
            if (fileName == null)
                throw new ArgumentNullException("fileName");
            int lineno = 0;
            while (true)
            {
                string line = reader.ReadLine();
                if (line == null)
                    break;
                ++lineno;
                string code = null;
                string[] items = line.Split();
                int index = 0;
                foreach (string item in items)
                {
                    if (item.Length > 0)
                    {
                        if (code == null)
                        {
                            code = item;
                            foreach (char c in code)
                            {
                                if (c < 'a' || c > 'z')
                                {
                                    string message = string.Format("编码'{0}'无效，因为它不是小写字母串。", code);
                                    throw new FormatException(FormatMessage(fileName, lineno, line, message));
                                }
                                else if (c == 'z')
                                {
                                    string message = string.Format("编码'{0}'无效，因为微软五笔不支持'z'键。", code);
                                    throw new FormatException(FormatMessage(fileName, lineno, line, message));
                                }
                            }
                            if (code.Length > 4)
                            {
                                string message = string.Format("编码'{0}'长度超过4，微软五笔不支持。", code);
                                throw new FormatException(FormatMessage(fileName, lineno, line, message));
                            }
                        }
                        else
                        {
                            ++index;
                            if (index > 0xFFFF)
                            {
                                string message = string.Format("编码'{0}'无效，因为它包含了太多词条，超过了65535个。", code);
                                throw new FormatException(FormatMessage(fileName, lineno, line, message));
                            }
                            AddRecord(code, item);
                        }
                    }
                }
            }
        }

        private string FormatMessage(string fileName, int lineno, string text, string message)
        {
            return string.Format("{0}:{1}: {2}\n{3}", fileName, lineno, message, text);
        }

        private void ValidateCode(string code)
        {
            if (string.IsNullOrWhiteSpace(code))
                throw new FormatException(CodeEmpty);
            if (code.Length > 4)
                throw new FormatException(CodeTooLong);
            foreach (char c in code)
                if (c < 'a' || c > 'z')
                    throw new FormatException(CodeNotLetterString);
                else if (c == 'z')
                    throw new FormatException(CodeContainsLetterZ);
        }

        private void ValidateValue(string value)
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new FormatException(ValueEmpty);
            foreach (char c in value)
                if (char.IsWhiteSpace(c))
                    throw new FormatException(ValueContainsWS);
        }

        private byte[] EncodeInt32(int value)
        {
            byte[] bytes = new byte[4];
            bytes[0] = (byte)(value & 0xFF);
            bytes[1] = (byte)(value >> 8 & 0xFF);
            bytes[2] = (byte)(value >> 16 & 0xFF);
            bytes[3] = (byte)(value >> 24 & 0xFF);
            return bytes;
        }

        private byte[] EncodeInt16(short value)
        {
            byte[] bytes = new byte[2];
            bytes[0] = (byte)(value & 0xFF);
            bytes[1] = (byte)(value >> 8 & 0xFF);
            return bytes;
        }

        private byte[] EncodeAsciiString(string value)
        {
            byte[] bytes = new byte[value.Length];
            for (int i = 0; i < value.Length; i++)
                bytes[i] = (byte)value[i];
            return bytes;
        }

        public byte[] Encode()
        {
            // Encode body
            int[] offsets = new int[26];
            char prev = (char)('a' - 1);
            List<byte> body = new List<byte>();
            foreach (var t in records)
            {
                ValidateCode(t.Key);
                while (prev < t.Key[0])
                {
                    prev++;
                    offsets[prev - 'a'] = body.Count;
                }
                int index = 0;
                foreach (string value in t.Value)
                {
                    ValidateValue(value);
                    byte[] bytes = EncodePair(t.Key, value, ++index);
                    body.AddRange(bytes);
                }
            }
            while (prev < 'z')
            {
                prev++;
                offsets[prev - 'a'] = body.Count;
            }

            // Encode table
            List<byte> buffer = new List<byte>();
            buffer.AddRange(EncodeAsciiString("imscwubi")); // File tag
            buffer.AddRange(EncodeInt16(1)); // Major version
            buffer.AddRange(EncodeInt16(1)); // Minor version
            buffer.AddRange(EncodeInt32(0x40)); // Offset of index
            buffer.AddRange(EncodeInt32(0xA8)); // Offset of table
            buffer.AddRange(EncodeInt32(0xA8 + body.Count)); // File size
            buffer.AddRange(EncodeInt32(0x78563412)); // Meaning is unknown yet, place a special value here.
            buffer.AddRange(new byte[0x24]); // Reserved bytes
            foreach (int offset in offsets)
                buffer.AddRange(EncodeInt32(offset));
            buffer.AddRange(body);
            return buffer.ToArray();
        }

        public byte[] EncodePair(string code, string value, int index)
        {
            if (index > 0xFFFF)
                throw new FormatException(IndexTooLarge);
            byte[] bytes = new byte[2 + 2 + 2 + 8 + value.Length * 2 + 2];
            if (bytes.Length > 0xFFFF)
                throw new FormatException(PairTooHuge);
            bytes[0] = (byte)(bytes.Length & 0xFF);
            bytes[1] = (byte)(bytes.Length >> 8 & 0xFF);
            bytes[2] = (byte)(index & 0xFF);
            bytes[3] = (byte)(index >> 8 & 0xFF);
            if (code.Length > 4)
                throw new FormatException(CodeTooLong);
            bytes[4] = (byte)(code.Length & 0xFF);
            bytes[5] = (byte)(code.Length >> 8 & 0xFF);
            for (int i = 0; i < code.Length; i++)
                bytes[6 + i * 2] = (byte)code[i];
            for (int i = 0; i < value.Length; i++)
            {
                bytes[14 + i * 2] = (byte)(value[i] & 0xFF);
                bytes[14 + i * 2 + 1] = (byte)(value[i] >> 8 & 0xFF);
            }
            return bytes;
        }

        public static byte[] LoadTextTableFile(string fileName)
        {
            MicrosoftWubiTable table = new MicrosoftWubiTable();
            using (TextReader reader = new StreamReader(fileName))
                table.Load(reader, fileName);
            return table.Encode();
        }

        public static byte[] LoadBinaryTableFile(string fileName)
        {
            byte[] bytes;
            using (Stream stream = new FileStream(fileName, FileMode.Open))
            {
                long size = stream.Length;
                bytes = new byte[size];
                stream.Read(bytes, 0, bytes.Length);
            }
            const string tag = "imscwubi";
            bool valid = true;
            if (bytes.Length < tag.Length)
                valid = false;
            else
            {
                for (int i = 0; i < tag.Length; i++)
                    if (bytes[i] != (int)tag[i])
                        valid = false;
            }
            if (!valid)
            {
                string message = string.Format("{0}: 二进制码表无效，因为它不以字节串'{1}'开头。", fileName, tag);
                throw new FormatException(message);
            }
            return bytes;
        }
    }
}
